<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>小说爬虫系统 - 解决跨域问题</title>
    <style>
        :root {
            --primary-color: #3498db;
            --secondary-color: #2c3e50;
            --accent-color: #e74c3c;
            --light-color: #ecf0f1;
            --dark-color: #2c3e50;
            --success-color: #2ecc71;
            --warning-color: #f39c12;
            --text-color: #333;
            --text-light: #777;
            --border-radius: 8px;
            --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            --transition: all 0.3s ease;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: var(--text-color);
            background-color: #f9f9f9;
            padding: 20px;
        }

        .container {
            max-width: 1000px;
            margin: 0 auto;
            background: white;
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
            padding: 20px;
        }

        header {
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 1px solid #eee;
        }

        h1 {
            color: var(--secondary-color);
            margin-bottom: 10px;
        }

        .description {
            color: var(--text-light);
            margin-bottom: 20px;
        }

        .input-group {
            margin-bottom: 20px;
        }

        label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
        }

        input[type="text"], select {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: var(--border-radius);
            font-size: 16px;
        }

        button {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 12px 20px;
            border-radius: var(--border-radius);
            cursor: pointer;
            font-size: 16px;
            transition: var(--transition);
            margin-right: 10px;
            margin-bottom: 10px;
        }

        button:hover {
            background-color: #2980b9;
        }

        button:disabled {
            background-color: #95a5a6;
            cursor: not-allowed;
        }

        .progress-container {
            margin: 20px 0;
            background-color: #f0f0f0;
            border-radius: var(--border-radius);
            overflow: hidden;
            height: 20px;
        }

        .progress-bar {
            height: 100%;
            background-color: var(--success-color);
            width: 0;
            transition: width 0.3s ease;
            text-align: center;
            color: white;
            font-size: 12px;
            line-height: 20px;
        }

        .stats {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        .stat-box {
            background-color: var(--light-color);
            padding: 15px;
            border-radius: var(--border-radius);
            flex: 1;
            min-width: 150px;
            margin: 0 10px 10px 0;
            text-align: center;
        }

        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: var(--primary-color);
        }

        .stat-label {
            font-size: 14px;
            color: var(--text-light);
        }

        .results {
            margin-top: 30px;
        }

        .chapter-list {
            max-height: 400px;
            overflow-y: auto;
            border: 1px solid #eee;
            border-radius: var(--border-radius);
            padding: 10px;
        }

        .chapter-item {
            padding: 10px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            cursor: pointer;
            transition: var(--transition);
        }

        .chapter-item:hover {
            background-color: #f5f5f5;
        }

        .chapter-item:last-child {
            border-bottom: none;
        }

        .chapter-content {
            margin-top: 15px;
            padding: 15px;
            border: 1px solid #eee;
            border-radius: var(--border-radius);
            background-color: #fafafa;
            max-height: 300px;
            overflow-y: auto;
            line-height: 1.8;
        }

        .actions {
            margin-top: 20px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        .btn-download {
            background-color: var(--success-color);
        }

        .btn-clear {
            background-color: var(--accent-color);
        }

        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 20px;
            background-color: var(--success-color);
            color: white;
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
            transform: translateX(100%);
            transition: transform 0.3s ease;
            z-index: 1000;
        }

        .notification.show {
            transform: translateX(0);
        }

        .notification.error {
            background-color: var(--accent-color);
        }

        .search-results {
            margin-top: 15px;
            border: 1px solid #eee;
            border-radius: var(--border-radius);
            max-height: 200px;
            overflow-y: auto;
        }

        .search-result-item {
            padding: 10px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            transition: var(--transition);
        }

        .search-result-item:hover {
            background-color: #f5f5f5;
        }

        .search-result-item:last-child {
            border-bottom: none;
        }

        .config-section {
            background-color: #f8f9fa;
            padding: 15px;
            border-radius: var(--border-radius);
            margin-bottom: 20px;
        }

        .config-title {
            font-weight: bold;
            margin-bottom: 10px;
            color: var(--secondary-color);
        }

        .cors-solution {
            background-color: #e8f4fd;
            padding: 15px;
            border-radius: var(--border-radius);
            margin-top: 20px;
        }

        .cors-solution h4 {
            color: var(--primary-color);
            margin-bottom: 10px;
        }

        .cors-solution ul {
            padding-left: 20px;
            margin-bottom: 10px;
        }

        .cors-solution li {
            margin-bottom: 8px;
        }

        @media (max-width: 768px) {
            .stats {
                flex-direction: column;
            }
            
            .stat-box {
                margin-right: 0;
                margin-bottom: 10px;
            }

            .actions {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Node.js 小说爬虫系统</h1>
            <p class="description">输入小说名称或URL，系统将自动爬取小说内容</p>
        </header>

        <div class="config-section">
            <div class="config-title">服务器配置</div>
            <div class="input-group">
                <label for="api-base">API 地址</label>
                <input type="text" id="api-base" value="http://localhost:3000/api" placeholder="例如: http://localhost:3000/api">
                <small>请确保与您的Node.js服务器地址一致</small>
            </div>
            <button onclick="testConnection()">测试连接</button>
        </div>

        <div class="input-group">
            <label for="novel-input">小说名称</label>
            <input type="text" id="novel-input" placeholder="例如: 剑来">
            <button onclick="searchNovel()" style="margin-top: 10px;">搜索小说</button>
        </div>

        <div class="search-results" id="search-results" style="display: none;">
            <!-- 搜索结果将在这里显示 -->
        </div>

        <div class="input-group">
            <label for="site-select">选择网站</label>
            <select id="site-select">
                <option value="qidian">起点中文网</option>
                <option value="biquge">笔趣阁</option>
                <option value="demo">演示模式</option>
            </select>
        </div>

        <button id="crawl-btn" onclick="startCrawling()">开始爬取</button>
        <button id="simulate-btn" onclick="simulateCrawling()">模拟爬取（演示）</button>

        <div id="progress-section" style="display: none;">
            <h3>爬取进度</h3>
            <div class="progress-container">
                <div class="progress-bar" id="progress-bar">0%</div>
            </div>
            <div class="stats">
                <div class="stat-box">
                    <div class="stat-value" id="chapters-count">0</div>
                    <div class="stat-label">章节数</div>
                </div>
                <div class="stat-box">
                    <div class="stat-value" id="words-count">0</div>
                    <div class="stat-label">总字数</div>
                </div>
                <div class="stat-box">
                    <div class="stat-value" id="time-elapsed">0s</div>
                    <div class="stat-label">耗时</div>
                </div>
            </div>
        </div>

        <div class="results" id="results-section" style="display: none;">
            <h3>爬取结果 - <span id="novel-title"></span></h3>
            <div class="chapter-list" id="chapter-list">
                <!-- 章节列表将在这里动态生成 -->
            </div>
            
            <div id="chapter-content" class="chapter-content" style="display: none;">
                <!-- 章节内容将在这里显示 -->
            </div>
            
            <div class="actions">
                <button class="btn-download" id="download-btn">下载全文</button>
                <button class="btn-clear" onclick="clearResults()">清除结果</button>
            </div>
        </div>

        <div class="cors-solution">
            <h4>跨域问题解决方案</h4>
            <p>如果遇到跨域错误，请在您的Node.js服务器中添加以下CORS配置：</p>
            <ul>
                <li>安装CORS包: <code>npm install cors</code></li>
                <li>在服务器代码中添加:
                    <pre><code>const cors = require('cors');
app.use(cors());</code></pre>
                </li>
                <li>或者手动设置响应头:
                    <pre><code>app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    next();
});</code></pre>
                </li>
            </ul>
        </div>
    </div>

    <div class="notification" id="notification">操作成功！</div>

    <script>
        // 全局变量
        let crawlStartTime;
        let updateInterval;
        let crawledChapters = [];
        let selectedNovel = null;

        // 获取API基础地址
        function getApiBase() {
            return document.getElementById('api-base').value.trim();
        }

        // 显示通知
        function showNotification(message, isError = false) {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.classList.toggle('error', isError);
            notification.classList.add('show');
            
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
        }

        // 测试连接
        async function testConnection() {
            const apiBase = getApiBase();
            
            try {
                const response = await fetch(apiBase + '/test', {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });
                
                if (response.ok) {
                    showNotification('连接成功！');
                } else {
                    showNotification('连接失败，请检查服务器', true);
                }
            } catch (error) {
                console.error('连接测试错误:', error);
                showNotification('连接失败: ' + error.message, true);
            }
        }

        // 搜索小说
        async function searchNovel() {
            const novelName = document.getElementById('novel-input').value.trim();
            if (!novelName) {
                showNotification('请输入小说名称', true);
                return;
            }

            const site = document.getElementById('site-select').value;
            const apiBase = getApiBase();
            
            try {
                const response = await fetch(apiBase + '/search', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ novelName, site })
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const data = await response.json();
                
                if (data.error) {
                    showNotification(data.error, true);
                    return;
                }
                
                displaySearchResults(data.results);
            } catch (error) {
                console.error('搜索错误:', error);
                showNotification(`搜索失败: ${error.message}`, true);
            }
        }

        // 显示搜索结果
        function displaySearchResults(results) {
            const resultsContainer = document.getElementById('search-results');
            
            if (!results || results.length === 0) {
                resultsContainer.innerHTML = '<div class="search-result-item">未找到相关小说</div>';
                resultsContainer.style.display = 'block';
                return;
            }
            
            let html = '';
            results.forEach(novel => {
                html += `
                    <div class="search-result-item" data-url="${novel.url}" data-site="${novel.site}">
                        <strong>${novel.title}</strong> - ${novel.author || '未知作者'}
                        <div>${novel.update || '未知更新'}</div>
                        <small>来源: ${novel.site}</small>
                    </div>
                `;
            });
            
            resultsContainer.innerHTML = html;
            resultsContainer.style.display = 'block';
            
            // 添加点击事件
            const items = resultsContainer.querySelectorAll('.search-result-item');
            items.forEach(item => {
                item.addEventListener('click', function() {
                    selectedNovel = {
                        url: this.dataset.url,
                        site: this.dataset.site,
                        title: this.querySelector('strong').textContent
                    };
                    
                    document.getElementById('novel-input').value = selectedNovel.title;
                    resultsContainer.style.display = 'none';
                    
                    showNotification(`已选择: ${selectedNovel.title}`);
                });
            });
        }

        // 开始爬取
        async function startCrawling() {
            const site = document.getElementById('site-select').value;
            
            // 如果是演示模式，使用模拟爬取
            if (site === 'demo') {
                simulateCrawling();
                return;
            }
            
            if (!selectedNovel) {
                showNotification('请先搜索并选择一本小说', true);
                return;
            }

            // 禁用按钮
            document.getElementById('crawl-btn').disabled = true;
            
            // 显示进度区域
            document.getElementById('progress-section').style.display = 'block';
            document.getElementById('results-section').style.display = 'none';
            
            // 重置进度
            document.getElementById('progress-bar').style.width = '0%';
            document.getElementById('progress-bar').textContent = '0%';
            document.getElementById('chapters-count').textContent = '0';
            document.getElementById('words-count').textContent = '0';
            document.getElementById('time-elapsed').textContent = '0s';
            
            // 开始计时
            crawlStartTime = new Date();
            updateInterval = setInterval(updateTimer, 1000);
            
            const apiBase = getApiBase();
            
            try {
                // 获取章节列表
                const chaptersResponse = await fetch(apiBase + '/chapters', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ 
                        novelUrl: selectedNovel.url, 
                        site: selectedNovel.site 
                    })
                });
                
                if (!chaptersResponse.ok) {
                    throw new Error(`HTTP error! status: ${chaptersResponse.status}`);
                }
                
                const chaptersData = await chaptersResponse.json();
                
                if (chaptersData.error) {
                    throw new Error(chaptersData.error);
                }
                
                // 模拟进度更新
                simulateProgress();
                
                // 开始爬取内容（限制前5章作为演示）
                const crawlResponse = await fetch(apiBase + '/crawl', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ 
                        novelUrl: selectedNovel.url, 
                        site: selectedNovel.site,
                        startChapter: 0,
                        endChapter: 5
                    })
                });
                
                if (!crawlResponse.ok) {
                    throw new Error(`HTTP error! status: ${crawlResponse.status}`);
                }
                
                const crawlData = await crawlResponse.json();
                
                if (crawlData.error) {
                    throw new Error(crawlData.error);
                }
                
                // 处理爬取结果
                processCrawlResult(crawlData);
                
            } catch (error) {
                console.error('爬取错误:', error);
                showNotification(`爬取失败: ${error.message}`, true);
                clearInterval(updateInterval);
                document.getElementById('crawl-btn').disabled = false;
                
                // 如果是跨域错误，提示用户
                if (error.message.includes('CORS') || error.message.includes('Network')) {
                    showNotification('可能是跨域问题，请检查服务器CORS配置', true);
                }
            }
        }

        // 模拟进度更新
        function simulateProgress() {
            let progress = 0;
            const progressBar = document.getElementById('progress-bar');
            const progressInterval = setInterval(() => {
                progress += 5;
                if (progress <= 100) {
                    progressBar.style.width = progress + '%';
                    progressBar.textContent = progress + '%';
                } else {
                    clearInterval(progressInterval);
                }
            }, 200);
        }

        // 处理爬取结果
        function processCrawlResult(data) {
            // 启用按钮
            document.getElementById('crawl-btn').disabled = false;
            
            // 停止计时
            clearInterval(updateInterval);
            
            // 保存爬取的章节
            crawledChapters = data.chapters || [];
            
            // 显示结果区域
            document.getElementById('results-section').style.display = 'block';
            document.getElementById('novel-title').textContent = data.title || '未知小说';
            
            // 更新统计信息
            document.getElementById('chapters-count').textContent = data.totalChapters || crawledChapters.length;
            document.getElementById('words-count').textContent = (data.totalWords || 0).toLocaleString();
            
            // 显示章节列表
            displayChapterList();
            
            showNotification(`爬取完成！共${crawledChapters.length}章`);
        }

        // 模拟爬取（演示模式）
        function simulateCrawling() {
            // 禁用按钮
            document.getElementById('crawl-btn').disabled = true;
            document.getElementById('simulate-btn').disabled = true;
            
            // 显示进度区域
            document.getElementById('progress-section').style.display = 'block';
            document.getElementById('results-section').style.display = 'none';
            
            // 重置进度
            document.getElementById('progress-bar').style.width = '0%';
            document.getElementById('progress-bar').textContent = '0%';
            document.getElementById('chapters-count').textContent = '0';
            document.getElementById('words-count').textContent = '0';
            document.getElementById('time-elapsed').textContent = '0s';
            
            // 开始计时
            crawlStartTime = new Date();
            updateInterval = setInterval(updateTimer, 1000);
            
            // 模拟进度
            simulateProgress();
            
            // 模拟爬取结果（2秒后）
            setTimeout(() => {
                // 生成模拟数据
                const mockChapters = [];
                for (let i = 1; i <= 5; i++) {
                    mockChapters.push({
                        title: `第${i}章 模拟章节标题`,
                        content: `这是第${i}章的模拟内容。此处显示的是演示数据，实际爬取时会从网站获取真实内容。小说爬虫系统正在正常工作。\n\n这是第二段模拟内容，用于展示章节内容的显示效果。`,
                        wordCount: Math.floor(Math.random() * 1000) + 500
                    });
                }
                
                const mockResult = {
                    title: '模拟小说',
                    totalChapters: 5,
                    totalWords: mockChapters.reduce((sum, chap) => sum + chap.wordCount, 0),
                    chapters: mockChapters
                };
                
                processCrawlResult(mockResult);
                document.getElementById('simulate-btn').disabled = false;
            }, 2000);
        }

        // 显示章节列表
        function displayChapterList() {
            const chapterList = document.getElementById('chapter-list');
            chapterList.innerHTML = '';
            
            crawledChapters.forEach((chapter, index) => {
                const chapterItem = document.createElement('div');
                chapterItem.className = 'chapter-item';
                chapterItem.innerHTML = `
                    <span>${chapter.title}</span>
                    <span>${chapter.wordCount ? chapter.wordCount.toLocaleString() : '未知'}字</span>
                `;
                
                // 添加点击事件查看内容
                chapterItem.addEventListener('click', () => {
                    showChapterContent(index);
                });
                
                chapterList.appendChild(chapterItem);
            });
            
            // 显示下载按钮
            document.getElementById('download-btn').onclick = downloadNovel;
        }

        // 显示章节内容
        function showChapterContent(index) {
            const contentDiv = document.getElementById('chapter-content');
            contentDiv.innerHTML = `
                <h4>${crawledChapters[index].title}</h4>
                <div style="white-space: pre-line;">${crawledChapters[index].content}</div>
            `;
            contentDiv.style.display = 'block';
            
            // 滚动到内容区域
            contentDiv.scrollIntoView({ behavior: 'smooth' });
        }

        // 更新计时器
        function updateTimer() {
            const now = new Date();
            const elapsedSeconds = Math.floor((now - crawlStartTime) / 1000);
            document.getElementById('time-elapsed').textContent = `${elapsedSeconds}s`;
        }

        // 下载小说
        function downloadNovel() {
            if (crawledChapters.length === 0) {
                showNotification('没有内容可下载', true);
                return;
            }
            
            let content = `书名：${document.getElementById('novel-title').textContent}\n\n`;
            
            crawledChapters.forEach(chapter => {
                content += `${chapter.title}\n\n`;
                content += `${chapter.content}\n\n`;
                content += '-'.repeat(50) + '\n\n';
            });
            
            const blob = new Blob([content], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `${document.getElementById('novel-title').textContent}.txt`;
            document.body.appendChild(a);
            a.click();
            
            // 清理
            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }, 100);
            
            showNotification('小说下载成功！');
        }

        // 清除结果
        function clearResults() {
            document.getElementById('novel-input').value = '';
            document.getElementById('search-results').style.display = 'none';
            document.getElementById('progress-section').style.display = 'none';
            document.getElementById('results-section').style.display = 'none';
            document.getElementById('chapter-list').innerHTML = '';
            document.getElementById('chapter-content').style.display = 'none';
            document.getElementById('chapter-content').innerHTML = '';
            
            selectedNovel = null;
            crawledChapters = [];
            
            showNotification('已清除所有结果');
        }
    </script>
</body>
</html>